package org.yuanqiframework.yuanqi.boot.auth;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.yuanqiframework.yuanqi.web.WebProperty;
import org.yuanqiframework.yuanqi.web.auth.AuthContext;
import org.yuanqiframework.yuanqi.web.constant.HttpStatus;
import org.yuanqiframework.yuanqi.web.entity.AjaxResult;
import org.yuanqiframework.yuanqi.web.utils.SpringMvcUtils;
import org.yuanqiframework.yuanqi.web.utils.SpringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @description: 基于数据库的权限认证方案
 * @author: 曹波涛
 * @create: 2025/8/20
 **/
@Slf4j
@Service
public class AuthService implements ApplicationRunner {
    private final String CACHE_KEY_UR = "sys_url_roles";
    private final String CACHE_KEY_UW = "sys_url_whitelist";
    @Resource
    private JdbcTemplate jdbcTemplate;
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    @Resource
    private WebProperty webProperty;

    public void flushUrlRolesCache() {
        List<Map<String, Object>> urlRoleMaps = jdbcTemplate.queryForList("SELECT\n" +
                "    sys_url.id,\n" +
                "    sys_role.role_code \n" +
                "FROM\n" +
                "    sys_url\n" +
                "\t\tLEFT JOIN sys_role_url on  sys_url.id = sys_role_url.url_id\n" +
                "\t\tLEFT JOIN sys_role  on sys_role_url.role_id = sys_role.id \n" +
                "WHERE 1=1\n" +
                "ORDER BY\n" +
                "    sys_url.id,\n" +
                "    role_id");
        HashMap<String, Set<String>> stringSetHashMap = new HashMap<>();
        urlRoleMaps.stream().collect(Collectors.groupingBy(map -> map.get("id").toString()))
                .forEach((id, maps) -> {
                    List<String> roles = maps.stream().map(map -> {
                        if (map.containsKey("role_code")) {
                            Object roleCode = map.get("role_code");
                            return roleCode != null ? (String) roleCode : null;
                        } else {
                            return null;
                        }
                    }).filter(Objects::nonNull).collect(Collectors.toList());
                    log.debug("{} {}", id, roles);
                    stringSetHashMap.put(id, new HashSet<>(roles));
                });
        redisTemplate.delete(CACHE_KEY_UR);
        redisTemplate.opsForHash().putAll(CACHE_KEY_UR, stringSetHashMap);
    }

    public void flushUrlWhitelistCache() {
        List<Map<String, Object>> urlWhitelistMaps = jdbcTemplate.queryForList("SELECT url_id,allow_anonymous from sys_url_whitelist");
        HashMap<String, Boolean> urlWhitelist = new HashMap<>();
        urlWhitelistMaps.forEach(map -> urlWhitelist.put((String) map.get("url_id"), "0".equals(map.get("allow_anonymous"))));
        redisTemplate.delete(CACHE_KEY_UW);
        redisTemplate.opsForHash().putAll(CACHE_KEY_UW, urlWhitelist);
    }

    public void flushCache() {
        flushUrlRolesCache();
        flushUrlWhitelistCache();
    }

    private Map<Object, Object> queryUrlWhitelistCache() {
        HashMap<Object, Object> m = null;
        m = (HashMap<Object, Object>) redisTemplate.opsForHash().entries(CACHE_KEY_UW);
        return m;
    }

    // 初始化
    public void initAuth() {
        List<Map<String, String>> allUrl = SpringUtils.getBean(SpringMvcUtils.class).getAllUrl();
        jdbcTemplate.execute("truncate table sys_url");
        //使用原生JDBC持久化到mysql
        saveBatchNative(allUrl);
        //持久化到redis map存储 key:id value:role_code
        flushCache();
        log.info("enable auth with db");
    }

    // 使用原生JDBC实现批量保存
    private void saveBatchNative(List<Map<String, String>> allUrl) {
        String sql = "INSERT INTO sys_url (id, http_method, http_url, class_name, method, created_at) VALUES (?, ?, ?, ?, ?, ?)";
        List<Object[]> batchArgs = new ArrayList<>();
        for (Map<String, String> map : allUrl) {
            Object[] args = new Object[6];
            args[0] = map.get("id");
            args[1] = map.get("httpMethod");
            args[2] = map.get("httpUrl");
            args[3] = map.get("className");
            args[4] = map.get("method");
            args[5] = new java.sql.Timestamp(System.currentTimeMillis());
            batchArgs.add(args);
        }
        try {
            jdbcTemplate.batchUpdate(sql, batchArgs);
            log.info("批量保存URL成功，URL数量{}", allUrl.size());
        } catch (Exception e) {
            log.error("批量保存URL失败", e);
            throw new RuntimeException("批量保存URL失败", e);
        }
    }

    public void registerAuthInterceptor(InterceptorRegistry registry) {
        if (!webProperty.getAuthWithDb()) {
            return;
        }
        HandlerInterceptor authInterceptor = new HandlerInterceptor() {
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                String httpMethod = request.getMethod();
                String httpUrl = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
                String apiBase64 = Base64.getEncoder().encodeToString((httpMethod + "<=>" + httpUrl).getBytes());
                //鉴权
                //是否登录
                boolean login = AuthContext.isLogin();
                //获取用户角色
                Set<String> userRoles = AuthContext.getRoles();
                userRoles = userRoles != null ? userRoles : new HashSet<>();
                if (login && userRoles.contains("admin")) {
                    //超级管理员放行所有接口
                    return true;
                }

                // 判断接口是否在白名单内部 apiBase64 查询 sys_url_white_list
                //如果存在，返回true 否则开始鉴权
                Map<Object, Object> urlWhitelist = queryUrlWhitelistCache();
                if (urlWhitelist.containsKey(apiBase64)) {
                    log.info("请求地址在白名单内 {} {}", httpMethod, httpUrl);
                    Boolean allowAnonymous = (Boolean) urlWhitelist.get(apiBase64);
                    if (allowAnonymous) {
                        //允许未登录访问
                        return true;
                    }
                    if (!login) {
                        log.warn("请求地址在白名单内，但未登录访问 {} {}", httpMethod, httpUrl);
                        response401(response);
                        return false;
                    } else {
                        return true;
                    }
                }

                if (!redisTemplate.opsForHash().hasKey(CACHE_KEY_UR, apiBase64)) {
                    //url不在处理的url中 404 或者其他资源
                    log.debug("未定义资源放行 {} {}", httpMethod, httpUrl);
                    return true;
                }
                Set<String> accessRoles = (Set<String>) redisTemplate.opsForHash().get(CACHE_KEY_UR, apiBase64);
                if (accessRoles == null) {
                    return true;
                }
                if (!login) {
                    log.warn("未登录访问 {} {}", httpMethod, httpUrl);
                    response401(response);
                    return false;
                }
                boolean flag = false;
                if (accessRoles.isEmpty()) {
                    log.debug("未配置在白名单，任何角色不可访问 {} {}", httpMethod, httpUrl);
                } else {
                    for (String accessRole : accessRoles) {
                        if (userRoles.contains(accessRole)) {
                            flag = true;
                            log.info("用户权限正常 {} 在 {} 内", userRoles, accessRoles);
                            break;
                        }
                    }
                }
                if (!flag) {
                    log.warn("用户权限不足 {} 不在 {} 内", userRoles, accessRoles);
                    response403(response);
                    return false;
                } else {
                    return true;
                }
            }
        };
        registry.addInterceptor(authInterceptor)
                .addPathPatterns("/**");
    }

    //响应
    private void response(HttpServletResponse response, int status, String message) {
        try {
            response.setStatus(HttpStatus.SUCCESS);
            response.setContentType("application/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write(JSON.toJSONString(AjaxResult.success(null, status, message), SerializerFeature.WriteMapNullValue));
            writer.flush();
            writer.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    //响应401
    private void response401(HttpServletResponse response) {
        response(response, HttpStatus.UNAUTHORIZED, "未认证，请登录后重试。");
    }

    //响应403
    private void response403(HttpServletResponse response) {
        response(response, HttpStatus.FORBIDDEN, "权限不足，拒绝访问。");
    }

    @Override
    public void run(ApplicationArguments args) {
        if (webProperty.getAuthWithDb()) {
            initAuth();
        }
    }
}
